package com.cheche.dn.common.base;

import com.cheche.dn.common.Filter;
import com.cheche.dn.common.Filter.Operator;
import com.cheche.dn.common.Order;
import com.cheche.dn.common.Page;
import com.cheche.dn.common.Pageable;
import com.google.common.collect.Lists;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import javax.persistence.*;
import javax.persistence.criteria.*;
import java.util.*;

/**
 * Base - Dao
 * Created by cheshun on 15/8/19.
 */
@Repository
@Transactional(readOnly = true)
public class Dao {

    /** 别名数 */
    private static volatile long aliasCount = 0;

    @PersistenceContext
    protected EntityManager entityManager;

    public <T> void persist(T entity) {
        Assert.notNull(entity);
        entityManager.persist(entity);
    }

    public <T> T merge(T entity) {
        Assert.notNull(entity);
        return entityManager.merge(entity);
    }

    public <T> int conditionUpdate(Class<T> entityClass, Map<String, Object> updateParams, List<Filter> filters) {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaUpdate<T> criteriaUpdate = criteriaBuilder.createCriteriaUpdate(entityClass);
        criteriaUpdate.from(entityClass);
        addRestrictions(criteriaUpdate, filters);
        if (BaseEntity.class.isAssignableFrom(entityClass)) {
            criteriaUpdate.set(BaseEntity.MODIFY_DATE_PROPERTY_NAME, System.currentTimeMillis());
        }
        for (String name : updateParams.keySet()) {
            criteriaUpdate.set(name, updateParams.get(name));
        }
        return entityManager.createQuery(criteriaUpdate).executeUpdate();
    }

    public <T> void remove(T entity) {
        if (entity != null) {
            if (entityManager.contains(entity))
                entityManager.remove(entity);
            else
                entityManager.remove(entityManager.merge(entity));
        }
    }

    public <T> void refresh(T entity) {
        if (entity != null) {
            entityManager.refresh(entity);
        }
    }

    public <T> void refresh(T entity, LockModeType lockModeType) {
        if (entity != null) {
            if (lockModeType != null) {
                entityManager.refresh(entity, lockModeType);
            } else {
                entityManager.refresh(entity);
            }
        }
    }

    @SuppressWarnings("unchecked")
    public <T> Long getIdentifier(T entity) {
        Assert.notNull(entity);
        return (Long) entityManager.getEntityManagerFactory().getPersistenceUnitUtil().getIdentifier(entity);
    }

    public <T> boolean isManaged(T entity) {
        return entityManager.contains(entity);
    }

    public <T> void detach(T entity) {
        entityManager.detach(entity);
    }

    public <T> void lock(T entity, LockModeType lockModeType) {
        if (entity != null && lockModeType != null) {
            entityManager.lock(entity, lockModeType);
        }
    }

    public void clear() {
        entityManager.clear();
    }

    public void flush() {
        entityManager.flush();
    }

    public <T> T find(Class<T> entityClass, Long id) {
        if (id != null) {
            return entityManager.find(entityClass, id);
        }
        return null;
    }

    public <T> T find(Class<T> entityClass, Long id, LockModeType lockModeType) {
        if (id != null) {
            if (lockModeType != null) {
                return entityManager.find(entityClass, id, lockModeType);
            } else {
                return entityManager.find(entityClass, id);
            }
        }
        return null;
    }

    public <T> T find(Class<T> entityClass, Filter... filters) {
        if (filters == null) {
            return null;
        }
        List<T> list = findList(entityClass, 0, 1, Arrays.asList(filters), null);
        return list.size() > 0 ? list.get(0) : null;
    }

    public <T> List<T> findList(Class<T> entityClass, Integer first, Integer count, List<Filter> filters, List<Order> orders) {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<T> criteriaQuery = criteriaBuilder.createQuery(entityClass);
        criteriaQuery.select(criteriaQuery.from(entityClass));
        return findList(criteriaQuery, first, count, filters, orders);
    }

    public <T> Page<T> findPage(Class<T> entityClass, Pageable pageable) {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<T> criteriaQuery = criteriaBuilder.createQuery(entityClass);
        criteriaQuery.select(criteriaQuery.from(entityClass));
        return findPage(criteriaQuery, pageable);
    }

    public <T> long count(Class<T> entityClass, Filter... filters) {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<T> criteriaQuery = criteriaBuilder.createQuery(entityClass);
        criteriaQuery.select(criteriaQuery.from(entityClass));
        return count(criteriaQuery, filters != null ? Arrays.asList(filters) : null);
    }

    public <T> Number sum(Class<T> entityClass, String sumProperty, Filter... filters) {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<Number> criteriaQuery = criteriaBuilder.createQuery(Number.class);
        Root<T> root = criteriaQuery.from(entityClass);
        if (filters != null) {
            criteriaQuery.where(createRestrictions(criteriaQuery.getRestriction(), root, Arrays.asList(filters)));
        }
        criteriaQuery.select(criteriaBuilder.sum(root.<Number>get(sumProperty)));
        return entityManager.createQuery(criteriaQuery).setFlushMode(FlushModeType.COMMIT).getSingleResult();
    }

    protected <T> List<T> findList(CriteriaQuery<T> criteriaQuery, Integer first, Integer count, List<Filter> filters, List<Order> orders) {
        Assert.notNull(criteriaQuery);
        Assert.notNull(criteriaQuery.getSelection());
        Assert.notEmpty(criteriaQuery.getRoots());

        Root<T> root = getRoot(criteriaQuery);
        if (root == null) {
            return Collections.emptyList();
        }
        addRestrictions(criteriaQuery, filters);
        addOrders(criteriaQuery, orders);
        if (criteriaQuery.getOrderList().isEmpty()) {
            addDefaultOrders(criteriaQuery, root);
        }
        TypedQuery<T> query = entityManager.createQuery(criteriaQuery).setFlushMode(FlushModeType.COMMIT);
        if (first != null) {
            query.setFirstResult(first);
        }
        if (count != null) {
            query.setMaxResults(count);
        }
        return query.getResultList();
    }

    protected <T> Page<T> findPage(CriteriaQuery<T> criteriaQuery, Pageable pageable) {
        Assert.notNull(criteriaQuery);
        Assert.notNull(criteriaQuery.getSelection());
        Assert.notEmpty(criteriaQuery.getRoots());

        if (pageable == null) {
            pageable = new Pageable.Builder().build();
        }
        Root<T> root = getRoot(criteriaQuery);
        addRestrictions(criteriaQuery, pageable);
        addOrders(criteriaQuery, pageable);
        if (criteriaQuery.getOrderList().isEmpty()) {
            addDefaultOrders(criteriaQuery, root);
        }
        long total = count(criteriaQuery, null);
        TypedQuery<T> query = entityManager.createQuery(criteriaQuery).setFlushMode(FlushModeType.COMMIT);
        query.setFirstResult((pageable.getPageNumber() - 1) * pageable.getPageSize());
        query.setMaxResults(pageable.getPageSize());
        return new Page<T>(query.getResultList(), total, pageable);
    }

    protected <T> Long count(CriteriaQuery<T> criteriaQuery, List<Filter> filters) {
        Assert.notNull(criteriaQuery);
        Assert.notNull(criteriaQuery.getSelection());
        Assert.notEmpty(criteriaQuery.getRoots());

        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        addRestrictions(criteriaQuery, filters);

        CriteriaQuery<Long> countCriteriaQuery = criteriaBuilder.createQuery(Long.class);
        for (Root<?> root : criteriaQuery.getRoots()) {
            Root<?> dest = countCriteriaQuery.from(root.getJavaType());
            dest.alias(getAlias(root));
            copyJoins(root, dest);
        }

        Root<?> countRoot = getRoot(countCriteriaQuery, criteriaQuery.getResultType());
        countCriteriaQuery.select(criteriaBuilder.count(countRoot));

        if (criteriaQuery.getGroupList() != null) {
            countCriteriaQuery.groupBy(criteriaQuery.getGroupList());
        }
        if (criteriaQuery.getGroupRestriction() != null) {
            countCriteriaQuery.having(criteriaQuery.getGroupRestriction());
        }
        if (criteriaQuery.getRestriction() != null) {
            countCriteriaQuery.where(criteriaQuery.getRestriction());
        }
        return entityManager.createQuery(countCriteriaQuery).setFlushMode(FlushModeType.COMMIT).getSingleResult();
    }

    private void copyJoins(From<?, ?> from, From<?, ?> to) {
        for (Join<?, ?> join : from.getJoins()) {
            Join<?, ?> toJoin = to.join(join.getAttribute().getName(), join.getJoinType());
            toJoin.alias(getAlias(join));
            copyJoins(join, toJoin);
        }
        for (Fetch<?, ?> fetch : from.getFetches()) {
            Fetch<?, ?> toFetch = to.fetch(fetch.getAttribute().getName());
            copyFetches(fetch, toFetch);
        }
    }

    private void copyFetches(Fetch<?, ?> from, Fetch<?, ?> to) {
        for (Fetch<?, ?> fetch : from.getFetches()) {
            Fetch<?, ?> toFetch = to.fetch(fetch.getAttribute().getName());
            copyFetches(fetch, toFetch);
        }
    }

    private synchronized String getAlias(Selection<?> selection) {
        if (selection != null) {
            String alias = selection.getAlias();
            if (alias == null) {
                if (aliasCount >= 1000) {
                    aliasCount = 0;
                }
                alias = "DNAlias" + aliasCount++;
                selection.alias(alias);
            }
            return alias;
        }
        return null;
    }

    private <T> Root<T> getRoot(CriteriaQuery<T> criteriaQuery) {
        if (criteriaQuery != null) {
            return getRoot(criteriaQuery, criteriaQuery.getResultType());
        }
        return null;
    }

    private <T> Root<T> getRoot(CriteriaQuery<?> criteriaQuery, Class<T> clazz) {
        if (criteriaQuery != null && criteriaQuery.getRoots() != null && clazz != null) {
            for (Root<?> root : criteriaQuery.getRoots()) {
                if (clazz.equals(root.getJavaType())) {
                    return (Root<T>) root.as(clazz);
                }
            }
        }
        return null;
    }

    private <T> void addRestrictions(CriteriaQuery<T> criteriaQuery, List<Filter> filters) {
        if (criteriaQuery == null || filters == null || filters.isEmpty()) {
            return;
        }
        Predicate restrictions = createRestrictions(criteriaQuery, filters);
        if (restrictions == null) return;
        criteriaQuery.where(restrictions);
    }

    private <T> void addRestrictions(CriteriaQuery<T> criteriaQuery, Pageable pageable) {
        if (criteriaQuery == null || pageable == null) {
            return;
        }
        Predicate restrictions = createRestrictions(criteriaQuery, pageable.getFilters());
        if (restrictions == null) return;
        criteriaQuery.where(restrictions);
    }

    private <T> void addRestrictions(CriteriaUpdate<T> criteriaUpdate, List<Filter> filters) {
        if (criteriaUpdate == null || filters == null || filters.isEmpty()) {
            return;
        }
        Predicate restrictions = createRestrictions(criteriaUpdate, filters);
        if (restrictions == null) return;
        criteriaUpdate.where(restrictions);
    }

    private <T> Predicate createRestrictions(CriteriaQuery<T> criteriaQuery, List<Filter> filters) {
        Root<T> root = getRoot(criteriaQuery);
        if (root == null) {
            return null;
        }
        return createRestrictions(criteriaQuery.getRestriction(), root, filters);
    }

    private <T> Predicate createRestrictions(CriteriaUpdate<T> criteriaUpdate, List<Filter> filters) {
        Root<T> root = criteriaUpdate.getRoot();
        if (root == null) {
            return null;
        }
        return createRestrictions(criteriaUpdate.getRestriction(), root, filters);
    }

    private <T> Predicate createRestrictions(Predicate restrictions, Root<T> root, List<Filter> filters) {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        if (restrictions == null) {
            restrictions = criteriaBuilder.conjunction();
        }
        for (Filter filter : filters) {
            Predicate predicate = createPredicate(criteriaBuilder, root, filter);
            if (predicate == null) continue;
            restrictions = criteriaBuilder.and(restrictions, predicate);
        }
        return restrictions;
    }

    private <T> Predicate createPredicate(CriteriaBuilder criteriaBuilder, Root<T> root, Filter filter) {
        if (filter == null || (StringUtils.isEmpty(filter.getProperty()) && filter.getOperator() != Operator.or)) {
            return null;
        }
        if (filter.getOperator() == Operator.eq && filter.getValue() != null) {
            if (filter.getIgnoreCase() != null && filter.getIgnoreCase() && filter.getValue() instanceof String) {
                return criteriaBuilder.equal(criteriaBuilder.lower(root.<String> get(filter.getProperty())), ((String) filter.getValue()).toLowerCase());
            } else {
                return criteriaBuilder.equal(root.get(filter.getProperty()), filter.getValue());
            }
        } else if (filter.getOperator() == Operator.ne && filter.getValue() != null) {
            if (filter.getIgnoreCase() != null && filter.getIgnoreCase() && filter.getValue() instanceof String) {
                return criteriaBuilder.notEqual(criteriaBuilder.lower(root.<String> get(filter.getProperty())), ((String) filter.getValue()).toLowerCase());
            } else {
                return criteriaBuilder.notEqual(root.get(filter.getProperty()), filter.getValue());
            }
        } else if (filter.getOperator() == Operator.gt && filter.getValue() != null) {
            return criteriaBuilder.gt(root.<Number> get(filter.getProperty()), (Number) filter.getValue());
        } else if (filter.getOperator() == Operator.lt && filter.getValue() != null) {
            return criteriaBuilder.lt(root.<Number> get(filter.getProperty()), (Number) filter.getValue());
        } else if (filter.getOperator() == Operator.ge && filter.getValue() != null) {
            return criteriaBuilder.ge(root.<Number> get(filter.getProperty()), (Number) filter.getValue());
        } else if (filter.getOperator() == Operator.le && filter.getValue() != null) {
            return criteriaBuilder.le(root.<Number> get(filter.getProperty()), (Number) filter.getValue());
        } else if (filter.getOperator() == Operator.like && filter.getValue() != null && filter.getValue() instanceof String) {
            return criteriaBuilder.like(root.<String> get(filter.getProperty()), (String) filter.getValue());
        } else if (filter.getOperator() == Operator.in && filter.getValue() != null) {
            return root.get(filter.getProperty()).in((Collection<?>) filter.getValue());
        } else if (filter.getOperator() == Operator.isNull) {
            return root.get(filter.getProperty()).isNull();
        } else if (filter.getOperator() == Operator.isNotNull) {
            return root.get(filter.getProperty()).isNotNull();
        } else if (filter.getOperator() == Operator.or && !filter.getOrSet().isEmpty()) {
            Predicate[] predicates = new Predicate[filter.getOrSet().size()];
            int i = 0;
            for (Filter f : filter.getOrSet()) {
                predicates[i] = createPredicate(criteriaBuilder, root, f);
                i ++;
            }
            return criteriaBuilder.or(predicates);
        }
        return null;
    }

    private <T> void addDefaultOrders(CriteriaQuery<T> criteriaQuery, Root<T> root) {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        if (OrderEntity.class.isAssignableFrom(criteriaQuery.getResultType())) {
            criteriaQuery.orderBy(criteriaBuilder.desc(root.get(OrderEntity.ORDER_PROPERTY_NAME)));
        } else if (BaseEntity.class.isAssignableFrom(criteriaQuery.getResultType())) {
            criteriaQuery.orderBy(criteriaBuilder.desc(root.get(OrderEntity.CREATE_DATE_PROPERTY_NAME)));
        } else {
            criteriaQuery.orderBy(criteriaBuilder.asc(root.get(OrderEntity.ID_PROPERTY_NAME)));
        }
    }

    private <T> void addOrders(CriteriaQuery<T> criteriaQuery, Pageable pageable) {
        if (criteriaQuery == null || pageable == null) {
            return;
        }
        addOrders(criteriaQuery, pageable.getOrders());
    }

    private <T> void addOrders(CriteriaQuery<T> criteriaQuery, List<Order> orders) {
        if (criteriaQuery == null || orders == null || orders.isEmpty()) {
            return;
        }
        criteriaQuery.orderBy(createOrders(criteriaQuery, orders));
    }

    private <T> List<javax.persistence.criteria.Order> createOrders(CriteriaQuery<T> criteriaQuery, List<Order> orders) {
        Root<T> root = getRoot(criteriaQuery);
        if (root == null) {
            return Collections.emptyList();
        }
        List<javax.persistence.criteria.Order> orderList = Lists.newArrayList();
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        if (!criteriaQuery.getOrderList().isEmpty()) {
            orderList.addAll(criteriaQuery.getOrderList());
        }
        for (Order order : orders) {
            if (order.getDirection() == Order.Direction.asc) {
                orderList.add(criteriaBuilder.asc(root.get(order.getProperty())));
            } else if (order.getDirection() == Order.Direction.desc) {
                orderList.add(criteriaBuilder.desc(root.get(order.getProperty())));
            }
        }
        return orderList;
    }

}
